/*
 *  PCM Interface - mmap
 *  Copyright (c) 2000 by Abramo Bagnara <abramo@alsa-project.org>
 *
 *   This library is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as
 *   published by the Free Software Foundation; either version 2.1 of
 *   the License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with this library; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 */

#include "config.h"
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <poll.h>
#include <sys/mman.h>
#ifdef HAVE_SYS_SHM_H
#include <sys/shm.h>
#endif
#include "pcm_local.h"

void snd_pcm_mmap_appl_backward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
	snd_pcm_sframes_t appl_ptr = *pcm->appl.ptr;
	appl_ptr -= frames;
	if (appl_ptr < 0)
		appl_ptr += pcm->boundary;
	*pcm->appl.ptr = appl_ptr;
}

void snd_pcm_mmap_appl_forward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
	snd_pcm_uframes_t appl_ptr = *pcm->appl.ptr;
	appl_ptr += frames;
	if (appl_ptr >= pcm->boundary)
		appl_ptr -= pcm->boundary;
	*pcm->appl.ptr = appl_ptr;
}

void snd_pcm_mmap_hw_backward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
	snd_pcm_sframes_t hw_ptr = *pcm->hw.ptr;
	hw_ptr -= frames;
	if (hw_ptr < 0)
		hw_ptr += pcm->boundary;
	*pcm->hw.ptr = hw_ptr;
}

void snd_pcm_mmap_hw_forward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
	snd_pcm_uframes_t hw_ptr = *pcm->hw.ptr;
	hw_ptr += frames;
	if (hw_ptr >= pcm->boundary)
		hw_ptr -= pcm->boundary;
	*pcm->hw.ptr = hw_ptr;
}

static snd_pcm_sframes_t snd_pcm_mmap_write_areas(snd_pcm_t *pcm,
						  const snd_pcm_channel_area_t *areas,
						  snd_pcm_uframes_t offset,
						  snd_pcm_uframes_t size)
{
	snd_pcm_uframes_t xfer = 0;

	if (snd_pcm_mmap_playback_avail(pcm) < size) {
		SNDMSG("too short avail %ld to size %ld", snd_pcm_mmap_playback_avail(pcm), size);
		return -EPIPE;
	}
	while (size > 0) {
		const snd_pcm_channel_area_t *pcm_areas;
		snd_pcm_uframes_t pcm_offset;
		snd_pcm_uframes_t frames = size;
		snd_pcm_sframes_t result;

		__snd_pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
		snd_pcm_areas_copy(pcm_areas, pcm_offset,
				   areas, offset, 
				   pcm->channels, 
				   frames, pcm->format);
		result = __snd_pcm_mmap_commit(pcm, pcm_offset, frames);
		if (result < 0)
			return xfer > 0 ? (snd_pcm_sframes_t)xfer : result;
		offset += result;
		xfer += result;
		size -= result;
	}
	return (snd_pcm_sframes_t)xfer;
}

static snd_pcm_sframes_t snd_pcm_mmap_read_areas(snd_pcm_t *pcm,
						 const snd_pcm_channel_area_t *areas,
						 snd_pcm_uframes_t offset,
						 snd_pcm_uframes_t size)
{
	snd_pcm_uframes_t xfer = 0;

	if (snd_pcm_mmap_capture_avail(pcm) < size) {
		SNDMSG("too short avail %ld to size %ld", snd_pcm_mmap_capture_avail(pcm), size);
		return -EPIPE;
	}
	while (size > 0) {
		const snd_pcm_channel_area_t *pcm_areas;
		snd_pcm_uframes_t pcm_offset;
		snd_pcm_uframes_t frames = size;
		snd_pcm_sframes_t result;
		
		__snd_pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
		snd_pcm_areas_copy(areas, offset,
				   pcm_areas, pcm_offset,
				   pcm->channels, 
				   frames, pcm->format);
		result = __snd_pcm_mmap_commit(pcm, pcm_offset, frames);
		if (result < 0)
			return xfer > 0 ? (snd_pcm_sframes_t)xfer : result;
		offset += result;
		xfer += result;
		size -= result;
	}
	return (snd_pcm_sframes_t)xfer;
}

/**
 * \brief Write interleaved frames to a PCM using direct buffer (mmap)
 * \param pcm PCM handle
 * \param buffer frames containing buffer
 * \param size frames to be written
 * \return a positive number of frames actually written otherwise a
 * negative error code
 * \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
 * \retval -EPIPE an underrun occurred
 * \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
 *
 * If the blocking behaviour is selected, then routine waits until
 * all requested bytes are played or put to the playback ring buffer.
 * The count of bytes can be less only if a signal or underrun occurred.
 *
 * If the non-blocking behaviour is selected, then routine doesn't wait at all.
 */
snd_pcm_sframes_t snd_pcm_mmap_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size)
{
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_areas_from_buf(pcm, areas, (void*)buffer);
	return snd_pcm_write_areas(pcm, areas, 0, size,
				   snd_pcm_mmap_write_areas);
}

/**
 * \brief Write non interleaved frames to a PCM using direct buffer (mmap)
 * \param pcm PCM handle
 * \param bufs frames containing buffers (one for each channel)
 * \param size frames to be written
 * \return a positive number of frames actually written otherwise a
 * negative error code
 * \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
 * \retval -EPIPE an underrun occurred
 * \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
 *
 * If the blocking behaviour is selected, then routine waits until
 * all requested bytes are played or put to the playback ring buffer.
 * The count of bytes can be less only if a signal or underrun occurred.
 *
 * If the non-blocking behaviour is selected, then routine doesn't wait at all.
 */
snd_pcm_sframes_t snd_pcm_mmap_writen(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size)
{
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_areas_from_bufs(pcm, areas, bufs);
	return snd_pcm_write_areas(pcm, areas, 0, size,
				   snd_pcm_mmap_write_areas);
}

/**
 * \brief Read interleaved frames from a PCM using direct buffer (mmap)
 * \param pcm PCM handle
 * \param buffer frames containing buffer
 * \param size frames to be written
 * \return a positive number of frames actually read otherwise a
 * negative error code
 * \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
 * \retval -EPIPE an overrun occurred
 * \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
 *
 * If the blocking behaviour was selected, then routine waits until
 * all requested bytes are filled. The count of bytes can be less only
 * if a signal or underrun occurred.
 *
 * If the non-blocking behaviour is selected, then routine doesn't wait at all.
 */
snd_pcm_sframes_t snd_pcm_mmap_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size)
{
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_areas_from_buf(pcm, areas, buffer);
	return snd_pcm_read_areas(pcm, areas, 0, size,
				  snd_pcm_mmap_read_areas);
}

/**
 * \brief Read non interleaved frames to a PCM using direct buffer (mmap)
 * \param pcm PCM handle
 * \param bufs frames containing buffers (one for each channel)
 * \param size frames to be written
 * \return a positive number of frames actually read otherwise a
 * negative error code
 * \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
 * \retval -EPIPE an overrun occurred
 * \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
 *
 * If the blocking behaviour was selected, then routine waits until
 * all requested bytes are filled. The count of bytes can be less only
 * if a signal or underrun occurred.
 *
 * If the non-blocking behaviour is selected, then routine doesn't wait at all.
 */
snd_pcm_sframes_t snd_pcm_mmap_readn(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size)
{
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_areas_from_bufs(pcm, areas, bufs);
	return snd_pcm_read_areas(pcm, areas, 0, size,
				  snd_pcm_mmap_read_areas);
}

int snd_pcm_channel_info_shm(snd_pcm_t *pcm, snd_pcm_channel_info_t *info, int shmid)
{
	switch (pcm->access) {
	case SND_PCM_ACCESS_MMAP_INTERLEAVED:
	case SND_PCM_ACCESS_RW_INTERLEAVED:
		info->first = info->channel * pcm->sample_bits;
		info->step = pcm->frame_bits;
		break;
	case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
	case SND_PCM_ACCESS_RW_NONINTERLEAVED:
		info->first = 0;
		info->step = pcm->sample_bits;
		break;
	default:
		SNDMSG("invalid access type %d", pcm->access);
		return -EINVAL;
	}
	info->addr = 0;
	if (pcm->hw_flags & SND_PCM_HW_PARAMS_EXPORT_BUFFER) {
		info->type = SND_PCM_AREA_SHM;
		info->u.shm.shmid = shmid;
		info->u.shm.area = NULL;
	} else
		info->type = SND_PCM_AREA_LOCAL;
	return 0;
}	

int snd_pcm_mmap(snd_pcm_t *pcm)
{
	int err;
	unsigned int c;
	assert(pcm);
	if (CHECK_SANITY(! pcm->setup)) {
		SNDMSG("PCM not set up");
		return -EIO;
	}
	if (CHECK_SANITY(pcm->mmap_channels || pcm->running_areas)) {
		SNDMSG("Already mmapped");
		return -EBUSY;
	}
	err = pcm->ops->mmap(pcm);
	if (err < 0)
		return err;
	if (pcm->mmap_shadow)
		return 0;
	pcm->mmap_channels = calloc(pcm->channels, sizeof(pcm->mmap_channels[0]));
	if (!pcm->mmap_channels)
		return -ENOMEM;
	pcm->running_areas = calloc(pcm->channels, sizeof(pcm->running_areas[0]));
	if (!pcm->running_areas) {
		free(pcm->mmap_channels);
		pcm->mmap_channels = NULL;
		return -ENOMEM;
	}
	for (c = 0; c < pcm->channels; ++c) {
		snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
		i->channel = c;
		err = snd_pcm_channel_info(pcm, i);
		if (err < 0) {
			free(pcm->mmap_channels);
			free(pcm->running_areas);
			pcm->mmap_channels = NULL;
			pcm->running_areas = NULL;
			return err;
		}
	}
	for (c = 0; c < pcm->channels; ++c) {
		snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
		snd_pcm_channel_area_t *a = &pcm->running_areas[c];
		char *ptr;
		size_t size;
		unsigned int c1;
		if (i->addr) {
        		a->addr = i->addr;
        		a->first = i->first;
        		a->step = i->step;
		        continue;
                }
                size = i->first + i->step * (pcm->buffer_size - 1) + pcm->sample_bits;
		for (c1 = c + 1; c1 < pcm->channels; ++c1) {
			snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
			size_t s;
			if (i1->type != i->type)
				continue;
			switch (i1->type) {
			case SND_PCM_AREA_MMAP:
				if (i1->u.mmap.fd != i->u.mmap.fd ||
				    i1->u.mmap.offset != i->u.mmap.offset)
					continue;
				break;
			case SND_PCM_AREA_SHM:
				if (i1->u.shm.shmid != i->u.shm.shmid)
					continue;
				break;
			case SND_PCM_AREA_LOCAL:
				break;
			default:
				assert(0);
			}
			s = i1->first + i1->step * (pcm->buffer_size - 1) + pcm->sample_bits;
			if (s > size)
				size = s;
		}
		size = (size + 7) / 8;
		size = page_align(size);
		switch (i->type) {
		case SND_PCM_AREA_MMAP:
			ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, i->u.mmap.fd, i->u.mmap.offset);
			if (ptr == MAP_FAILED) {
				SYSERR("mmap failed");
				return -errno;
			}
			i->addr = ptr;
			break;
		case SND_PCM_AREA_SHM:
#ifdef HAVE_SYS_SHM_H
			if (i->u.shm.shmid < 0) {
				int id;
				/* FIXME: safer permission? */
				id = shmget(IPC_PRIVATE, size, 0666);
				if (id < 0) {
					SYSERR("shmget failed");
					return -errno;
				}
				i->u.shm.shmid = id;
				ptr = shmat(i->u.shm.shmid, 0, 0);
				if (ptr == (void *) -1) {
					SYSERR("shmat failed");
					return -errno;
				}
				/* automatically remove segment if not used */
				if (shmctl(id, IPC_RMID, NULL) < 0){
					SYSERR("shmctl mark remove failed");
					return -errno;
				}
				i->u.shm.area = snd_shm_area_create(id, ptr);
				if (i->u.shm.area == NULL) {
					SYSERR("snd_shm_area_create failed");
					return -ENOMEM;
				}
				if (pcm->access == SND_PCM_ACCESS_MMAP_INTERLEAVED ||
				    pcm->access == SND_PCM_ACCESS_RW_INTERLEAVED) {
					unsigned int c1;
					for (c1 = c + 1; c1 < pcm->channels; c1++) {
						snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
						if (i1->u.shm.shmid < 0) {
							i1->u.shm.shmid = id;
							i1->u.shm.area = snd_shm_area_share(i->u.shm.area);
						}
					}
				}
			} else {
				ptr = shmat(i->u.shm.shmid, 0, 0);
				if (ptr == (void*) -1) {
					SYSERR("shmat failed");
					return -errno;
				}
			}
			i->addr = ptr;
			break;
#else
			SYSERR("shm support not available");
			return -ENOSYS;
#endif
		case SND_PCM_AREA_LOCAL:
			ptr = malloc(size);
			if (ptr == NULL) {
				SYSERR("malloc failed");
				return -errno;
			}
			i->addr = ptr;
			break;
		default:
			assert(0);
		}
		for (c1 = c + 1; c1 < pcm->channels; ++c1) {
			snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
			if (i1->type != i->type)
				continue;
			switch (i1->type) {
			case SND_PCM_AREA_MMAP:
				if (i1->u.mmap.fd != i->u.mmap.fd ||
                                    i1->u.mmap.offset != i->u.mmap.offset)
					continue;
				break;
			case SND_PCM_AREA_SHM:
				if (i1->u.shm.shmid != i->u.shm.shmid)
					continue;
				/* follow thru */
			case SND_PCM_AREA_LOCAL:
				if (pcm->access != SND_PCM_ACCESS_MMAP_INTERLEAVED &&
				    pcm->access != SND_PCM_ACCESS_RW_INTERLEAVED)
				        continue;
				break;
			default:
				assert(0);
			}
			i1->addr = i->addr;
		}
		a->addr = i->addr;
		a->first = i->first;
		a->step = i->step;
	}
	return 0;
}

int snd_pcm_munmap(snd_pcm_t *pcm)
{
	int err;
	unsigned int c;
	assert(pcm);
	if (CHECK_SANITY(! pcm->mmap_channels)) {
		SNDMSG("Not mmapped");
		return -ENXIO;
	}
	if (pcm->mmap_shadow)
		return pcm->ops->munmap(pcm);
	for (c = 0; c < pcm->channels; ++c) {
		snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
		unsigned int c1;
		size_t size = i->first + i->step * (pcm->buffer_size - 1) + pcm->sample_bits;
		if (!i->addr)
			continue;
		for (c1 = c + 1; c1 < pcm->channels; ++c1) {
			snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
			size_t s;
			if (i1->addr != i->addr)
				continue;
			i1->addr = NULL;
			s = i1->first + i1->step * (pcm->buffer_size - 1) + pcm->sample_bits;
			if (s > size)
				size = s;
		}
		size = (size + 7) / 8;
		size = page_align(size);
		switch (i->type) {
		case SND_PCM_AREA_MMAP:
			err = munmap(i->addr, size);
			if (err < 0) {
				SYSERR("mmap failed");
				return -errno;
			}
			errno = 0;
			break;
		case SND_PCM_AREA_SHM:
#ifdef HAVE_SYS_SHM_H
			if (i->u.shm.area) {
				snd_shm_area_destroy(i->u.shm.area);
				i->u.shm.area = NULL;
				if (pcm->access == SND_PCM_ACCESS_MMAP_INTERLEAVED ||
				    pcm->access == SND_PCM_ACCESS_RW_INTERLEAVED) {
					unsigned int c1;
					for (c1 = c + 1; c1 < pcm->channels; c1++) {
						snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
						if (i1->u.shm.area) {
							snd_shm_area_destroy(i1->u.shm.area);
							i1->u.shm.area = NULL;
						}
					}
				}
			}
			break;
#else
			SYSERR("shm support not available");
			return -ENOSYS;
#endif
		case SND_PCM_AREA_LOCAL:
			free(i->addr);
			break;
		default:
			assert(0);
		}
		i->addr = NULL;
	}
	err = pcm->ops->munmap(pcm);
	if (err < 0)
		return err;
	free(pcm->mmap_channels);
	free(pcm->running_areas);
	pcm->mmap_channels = NULL;
	pcm->running_areas = NULL;
	return 0;
}

/* called in pcm lock */
snd_pcm_sframes_t snd_pcm_write_mmap(snd_pcm_t *pcm, snd_pcm_uframes_t offset,
				     snd_pcm_uframes_t size)
{
	snd_pcm_uframes_t xfer = 0;
	snd_pcm_sframes_t err = 0;
	if (! size)
		return 0;
	while (xfer < size) {
		snd_pcm_uframes_t frames = size - xfer;
		snd_pcm_uframes_t cont = pcm->buffer_size - offset;
		if (cont < frames)
			frames = cont;
		switch (pcm->access) {
		case SND_PCM_ACCESS_MMAP_INTERLEAVED:
		{
			const snd_pcm_channel_area_t *a = snd_pcm_mmap_areas(pcm);
			const char *buf = snd_pcm_channel_area_addr(a, offset);
			snd_pcm_unlock(pcm); /* to avoid deadlock */
			err = _snd_pcm_writei(pcm, buf, frames);
			snd_pcm_lock(pcm);
			if (err >= 0)
				frames = err;
			break;
		}
		case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
		{
			unsigned int channels = pcm->channels;
			unsigned int c;
			void *bufs[channels];
			const snd_pcm_channel_area_t *areas = snd_pcm_mmap_areas(pcm);
			for (c = 0; c < channels; ++c) {
				const snd_pcm_channel_area_t *a = &areas[c];
				bufs[c] = snd_pcm_channel_area_addr(a, offset);
			}
			snd_pcm_unlock(pcm); /* to avoid deadlock */
			err = _snd_pcm_writen(pcm, bufs, frames);
			snd_pcm_lock(pcm);
			if (err >= 0)
				frames = err;
			break;
		}
		default:
			SNDMSG("invalid access type %d", pcm->access);
			return -EINVAL;
		}
		if (err < 0)
			break;
		xfer += frames;
		offset = (offset + frames) % pcm->buffer_size;
	}
	if (xfer > 0)
		return xfer;
	return err;
}

/* called in pcm lock */
snd_pcm_sframes_t snd_pcm_read_mmap(snd_pcm_t *pcm, snd_pcm_uframes_t offset,
				    snd_pcm_uframes_t size)
{
	snd_pcm_uframes_t xfer = 0;
	snd_pcm_sframes_t err = 0;
	if (! size)
		return 0;
	while (xfer < size) {
		snd_pcm_uframes_t frames = size - xfer;
		snd_pcm_uframes_t cont = pcm->buffer_size - offset;
		if (cont < frames)
			frames = cont;
		switch (pcm->access) {
		case SND_PCM_ACCESS_MMAP_INTERLEAVED:
		{
			const snd_pcm_channel_area_t *a = snd_pcm_mmap_areas(pcm);
			char *buf = snd_pcm_channel_area_addr(a, offset);
			snd_pcm_unlock(pcm); /* to avoid deadlock */
			err = _snd_pcm_readi(pcm, buf, frames);
			snd_pcm_lock(pcm);
			if (err >= 0)
				frames = err;
			break;
		}
		case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
		{
			snd_pcm_uframes_t channels = pcm->channels;
			unsigned int c;
			void *bufs[channels];
			const snd_pcm_channel_area_t *areas = snd_pcm_mmap_areas(pcm);
			for (c = 0; c < channels; ++c) {
				const snd_pcm_channel_area_t *a = &areas[c];
				bufs[c] = snd_pcm_channel_area_addr(a, offset);
			}
			snd_pcm_unlock(pcm); /* to avoid deadlock */
			err = _snd_pcm_readn(pcm->fast_op_arg, bufs, frames);
			snd_pcm_lock(pcm);
			if (err >= 0)
				frames = err;
			break;
		}
		default:
			SNDMSG("invalid access type %d", pcm->access);
			return -EINVAL;
		}
		if (err < 0)
			break;
		xfer += frames;
		offset = (offset + frames) % pcm->buffer_size;
	}
	if (xfer > 0)
		return xfer;
	return err;
}
